/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "PushNotifier.h"

#include "nsContentUtils.h"
#include "nsCOMPtr.h"
#include "nsICategoryManager.h"
#include "nsIXULRuntime.h"
#include "nsNetUtil.h"
#include "nsXPCOM.h"
#include "ServiceWorkerManager.h"

#include "mozilla/Services.h"
#include "mozilla/Unused.h"

#include "mozilla/dom/BodyUtil.h"
#include "mozilla/dom/ContentChild.h"
#include "mozilla/dom/ContentParent.h"

namespace mozilla {
namespace dom {

using workers::AssertIsOnMainThread;
using workers::ServiceWorkerManager;

PushNotifier::PushNotifier()
{}

PushNotifier::~PushNotifier()
{}

NS_IMPL_CYCLE_COLLECTION_0(PushNotifier)

NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PushNotifier)
  NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIPushNotifier)
  NS_INTERFACE_MAP_ENTRY(nsIPushNotifier)
NS_INTERFACE_MAP_END

NS_IMPL_CYCLE_COLLECTING_ADDREF(PushNotifier)
NS_IMPL_CYCLE_COLLECTING_RELEASE(PushNotifier)

NS_IMETHODIMP
PushNotifier::NotifyPushWithData(const nsACString& aScope,
                                 nsIPrincipal* aPrincipal,
                                 const nsAString& aMessageId,
                                 uint32_t aDataLen, uint8_t* aData)
{
  NS_ENSURE_ARG(aPrincipal);
  nsTArray<uint8_t> data;
  if (!data.SetCapacity(aDataLen, fallible)) {
    return NS_ERROR_OUT_OF_MEMORY;
  }
  if (!data.InsertElementsAt(0, aData, aDataLen, fallible)) {
    return NS_ERROR_OUT_OF_MEMORY;
  }
  PushMessageDispatcher dispatcher(aScope, aPrincipal, aMessageId, Some(data));
  return Dispatch(dispatcher);
}

NS_IMETHODIMP
PushNotifier::NotifyPush(const nsACString& aScope, nsIPrincipal* aPrincipal,
                         const nsAString& aMessageId)
{
  NS_ENSURE_ARG(aPrincipal);
  PushMessageDispatcher dispatcher(aScope, aPrincipal, aMessageId, Nothing());
  return Dispatch(dispatcher);
}

NS_IMETHODIMP
PushNotifier::NotifySubscriptionChange(const nsACString& aScope,
                                       nsIPrincipal* aPrincipal)
{
  NS_ENSURE_ARG(aPrincipal);
  PushSubscriptionChangeDispatcher dispatcher(aScope, aPrincipal);
  return Dispatch(dispatcher);
}

NS_IMETHODIMP
PushNotifier::NotifySubscriptionModified(const nsACString& aScope,
                                         nsIPrincipal* aPrincipal)
{
  NS_ENSURE_ARG(aPrincipal);
  PushSubscriptionModifiedDispatcher dispatcher(aScope, aPrincipal);
  return Dispatch(dispatcher);
}

NS_IMETHODIMP
PushNotifier::NotifyError(const nsACString& aScope, nsIPrincipal* aPrincipal,
                          const nsAString& aMessage, uint32_t aFlags)
{
  NS_ENSURE_ARG(aPrincipal);
  PushErrorDispatcher dispatcher(aScope, aPrincipal, aMessage, aFlags);
  return Dispatch(dispatcher);
}

nsresult
PushNotifier::Dispatch(PushDispatcher& aDispatcher)
{
  if (XRE_IsParentProcess()) {
    // Always notify XPCOM observers in the parent process.
    Unused << NS_WARN_IF(NS_FAILED(aDispatcher.NotifyObservers()));

    nsTArray<ContentParent*> contentActors;
    ContentParent::GetAll(contentActors);
    if (!contentActors.IsEmpty()) {
      // At least one content process is active, so e10s must be enabled.
      // Broadcast a message to notify observers and service workers.
      for (uint32_t i = 0; i < contentActors.Length(); ++i) {
        Unused << NS_WARN_IF(!aDispatcher.SendToChild(contentActors[i]));
      }
      return NS_OK;
    }

    if (BrowserTabsRemoteAutostart()) {
      // e10s is enabled, but no content processes are active.
      return aDispatcher.HandleNoChildProcesses();
    }

    // e10s is disabled; notify workers in the parent.
    return aDispatcher.NotifyWorkers();
  }

  // Otherwise, we're in the content process, so e10s must be enabled. Notify
  // observers and workers, then send a message to notify observers in the
  // parent.
  MOZ_ASSERT(XRE_IsContentProcess());

  nsresult rv = aDispatcher.NotifyObserversAndWorkers();

  ContentChild* parentActor = ContentChild::GetSingleton();
  if (!NS_WARN_IF(!parentActor)) {
    Unused << NS_WARN_IF(!aDispatcher.SendToParent(parentActor));
  }

  return rv;
}

PushData::PushData(const nsTArray<uint8_t>& aData)
  : mData(aData)
{}

PushData::~PushData()
{}

NS_IMPL_CYCLE_COLLECTION_0(PushData)

NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PushData)
  NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIPushData)
  NS_INTERFACE_MAP_ENTRY(nsIPushData)
NS_INTERFACE_MAP_END

NS_IMPL_CYCLE_COLLECTING_ADDREF(PushData)
NS_IMPL_CYCLE_COLLECTING_RELEASE(PushData)

nsresult
PushData::EnsureDecodedText()
{
  if (mData.IsEmpty() || !mDecodedText.IsEmpty()) {
    return NS_OK;
  }
  nsresult rv = BodyUtil::ConsumeText(
    mData.Length(),
    reinterpret_cast<uint8_t*>(mData.Elements()),
    mDecodedText
  );
  if (NS_WARN_IF(NS_FAILED(rv))) {
    mDecodedText.Truncate();
    return rv;
  }
  return NS_OK;
}

NS_IMETHODIMP
PushData::Text(nsAString& aText)
{
  nsresult rv = EnsureDecodedText();
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }
  aText = mDecodedText;
  return NS_OK;
}

NS_IMETHODIMP
PushData::Json(JSContext* aCx,
               JS::MutableHandle<JS::Value> aResult)
{
  nsresult rv = EnsureDecodedText();
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }
  ErrorResult error;
  BodyUtil::ConsumeJson(aCx, aResult, mDecodedText, error);
  return error.StealNSResult();
}

NS_IMETHODIMP
PushData::Binary(uint32_t* aDataLen, uint8_t** aData)
{
  NS_ENSURE_ARG_POINTER(aDataLen);
  NS_ENSURE_ARG_POINTER(aData);

  *aData = nullptr;
  if (mData.IsEmpty()) {
    *aDataLen = 0;
    return NS_OK;
  }
  uint32_t length = mData.Length();
  uint8_t* data = static_cast<uint8_t*>(NS_Alloc(length * sizeof(uint8_t)));
  if (!data) {
    return NS_ERROR_OUT_OF_MEMORY;
  }
  memcpy(data, mData.Elements(), length * sizeof(uint8_t));
  *aDataLen = length;
  *aData = data;
  return NS_OK;
}

PushMessage::PushMessage(nsIPrincipal* aPrincipal, nsIPushData* aData)
  : mPrincipal(aPrincipal)
  , mData(aData)
{}

PushMessage::~PushMessage()
{}

NS_IMPL_CYCLE_COLLECTION(PushMessage, mPrincipal, mData)

NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PushMessage)
  NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIPushMessage)
  NS_INTERFACE_MAP_ENTRY(nsIPushMessage)
NS_INTERFACE_MAP_END

NS_IMPL_CYCLE_COLLECTING_ADDREF(PushMessage)
NS_IMPL_CYCLE_COLLECTING_RELEASE(PushMessage)

NS_IMETHODIMP
PushMessage::GetPrincipal(nsIPrincipal** aPrincipal)
{
  NS_ENSURE_ARG_POINTER(aPrincipal);

  nsCOMPtr<nsIPrincipal> principal = mPrincipal;
  principal.forget(aPrincipal);
  return NS_OK;
}

NS_IMETHODIMP
PushMessage::GetData(nsIPushData** aData)
{
  NS_ENSURE_ARG_POINTER(aData);

  nsCOMPtr<nsIPushData> data = mData;
  data.forget(aData);
  return NS_OK;
}

PushDispatcher::PushDispatcher(const nsACString& aScope,
                               nsIPrincipal* aPrincipal)
  : mScope(aScope)
  , mPrincipal(aPrincipal)
{}

PushDispatcher::~PushDispatcher()
{}

nsresult
PushDispatcher::HandleNoChildProcesses()
{
  return NS_OK;
}

nsresult
PushDispatcher::NotifyObserversAndWorkers()
{
  Unused << NS_WARN_IF(NS_FAILED(NotifyObservers()));
  return NotifyWorkers();
}

bool
PushDispatcher::ShouldNotifyWorkers()
{
  if (NS_WARN_IF(!mPrincipal)) {
    return false;
  }
  // System subscriptions use observer notifications instead of service worker
  // events. The `testing.notifyWorkers` pref disables worker events for
  // non-system subscriptions.
  return !nsContentUtils::IsSystemPrincipal(mPrincipal) &&
         Preferences::GetBool("dom.push.testing.notifyWorkers", true);
}

nsresult
PushDispatcher::DoNotifyObservers(nsISupports *aSubject, const char *aTopic,
                                  const nsACString& aScope)
{
  nsCOMPtr<nsIObserverService> obsService =
    mozilla::services::GetObserverService();
  if (!obsService) {
    return NS_ERROR_FAILURE;
  }
  // If there's a service for this push category, make sure it is alive.
  nsCOMPtr<nsICategoryManager> catMan =
    do_GetService(NS_CATEGORYMANAGER_CONTRACTID);
  if (catMan) {
    nsXPIDLCString contractId;
    nsresult rv = catMan->GetCategoryEntry("push",
                                           mScope.BeginReading(),
                                           getter_Copies(contractId));
    if (NS_SUCCEEDED(rv)) {
      // Ensure the service is created - we don't need to do anything with
      // it though - we assume the service constructor attaches a listener.
      nsCOMPtr<nsISupports> service = do_GetService(contractId);
    }
  }
  return obsService->NotifyObservers(aSubject, aTopic,
                                     NS_ConvertUTF8toUTF16(mScope).get());
}

PushMessageDispatcher::PushMessageDispatcher(const nsACString& aScope,
                                             nsIPrincipal* aPrincipal,
                                             const nsAString& aMessageId,
                                             const Maybe<nsTArray<uint8_t>>& aData)
  : PushDispatcher(aScope, aPrincipal)
  , mMessageId(aMessageId)
  , mData(aData)
{}

PushMessageDispatcher::~PushMessageDispatcher()
{}

nsresult
PushMessageDispatcher::NotifyObservers()
{
  nsCOMPtr<nsIPushData> data;
  if (mData) {
    data = new PushData(mData.ref());
  }
  nsCOMPtr<nsIPushMessage> message = new PushMessage(mPrincipal, data);
  return DoNotifyObservers(message, OBSERVER_TOPIC_PUSH, mScope);
}

nsresult
PushMessageDispatcher::NotifyWorkers()
{
  if (!ShouldNotifyWorkers()) {
    return NS_OK;
  }
  RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
  if (!swm) {
    return NS_ERROR_FAILURE;
  }
  nsAutoCString originSuffix;
  nsresult rv = mPrincipal->GetOriginSuffix(originSuffix);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }
  return swm->SendPushEvent(originSuffix, mScope, mMessageId, mData);
}

bool
PushMessageDispatcher::SendToParent(ContentChild* aParentActor)
{
  if (mData) {
    return aParentActor->SendNotifyPushObserversWithData(mScope,
                                                         IPC::Principal(mPrincipal),
                                                         mMessageId,
                                                         mData.ref());
  }
  return aParentActor->SendNotifyPushObservers(mScope,
                                               IPC::Principal(mPrincipal),
                                               mMessageId);
}

bool
PushMessageDispatcher::SendToChild(ContentParent* aContentActor)
{
  if (mData) {
    return aContentActor->SendPushWithData(mScope, IPC::Principal(mPrincipal),
                                           mMessageId, mData.ref());
  }
  return aContentActor->SendPush(mScope, IPC::Principal(mPrincipal),
                                 mMessageId);
}

PushSubscriptionChangeDispatcher::PushSubscriptionChangeDispatcher(const nsACString& aScope,
                                                                   nsIPrincipal* aPrincipal)
  : PushDispatcher(aScope, aPrincipal)
{}

PushSubscriptionChangeDispatcher::~PushSubscriptionChangeDispatcher()
{}

nsresult
PushSubscriptionChangeDispatcher::NotifyObservers()
{
  return DoNotifyObservers(mPrincipal, OBSERVER_TOPIC_SUBSCRIPTION_CHANGE,
                           mScope);
}

nsresult
PushSubscriptionChangeDispatcher::NotifyWorkers()
{
  if (!ShouldNotifyWorkers()) {
    return NS_OK;
  }
  RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
  if (!swm) {
    return NS_ERROR_FAILURE;
  }
  nsAutoCString originSuffix;
  nsresult rv = mPrincipal->GetOriginSuffix(originSuffix);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }
  return swm->SendPushSubscriptionChangeEvent(originSuffix, mScope);
}

bool
PushSubscriptionChangeDispatcher::SendToParent(ContentChild* aParentActor)
{
  return aParentActor->SendNotifyPushSubscriptionChangeObservers(mScope,
                                                                 IPC::Principal(mPrincipal));
}

bool
PushSubscriptionChangeDispatcher::SendToChild(ContentParent* aContentActor)
{
  return aContentActor->SendPushSubscriptionChange(mScope,
                                                   IPC::Principal(mPrincipal));
}

PushSubscriptionModifiedDispatcher::PushSubscriptionModifiedDispatcher(const nsACString& aScope,
                                                                       nsIPrincipal* aPrincipal)
  : PushDispatcher(aScope, aPrincipal)
{}

PushSubscriptionModifiedDispatcher::~PushSubscriptionModifiedDispatcher()
{}

nsresult
PushSubscriptionModifiedDispatcher::NotifyObservers()
{
  return DoNotifyObservers(mPrincipal, OBSERVER_TOPIC_SUBSCRIPTION_MODIFIED,
                           mScope);
}

nsresult
PushSubscriptionModifiedDispatcher::NotifyWorkers()
{
  return NS_OK;
}

bool
PushSubscriptionModifiedDispatcher::SendToParent(ContentChild* aParentActor)
{
  return aParentActor->SendNotifyPushSubscriptionModifiedObservers(mScope,
                                                                   IPC::Principal(mPrincipal));
}

bool
PushSubscriptionModifiedDispatcher::SendToChild(ContentParent* aContentActor)
{
  return aContentActor->SendNotifyPushSubscriptionModifiedObservers(mScope,
                                                                    IPC::Principal(mPrincipal));
}

PushErrorDispatcher::PushErrorDispatcher(const nsACString& aScope,
                                         nsIPrincipal* aPrincipal,
                                         const nsAString& aMessage,
                                         uint32_t aFlags)
  : PushDispatcher(aScope, aPrincipal)
  , mMessage(aMessage)
  , mFlags(aFlags)
{}

PushErrorDispatcher::~PushErrorDispatcher()
{}

nsresult
PushErrorDispatcher::NotifyObservers()
{
  return NS_OK;
}

nsresult
PushErrorDispatcher::NotifyWorkers()
{
  if (!ShouldNotifyWorkers()) {
    // For system subscriptions, log the error directly to the browser console.
    return nsContentUtils::ReportToConsoleNonLocalized(mMessage,
                                                       mFlags,
                                                       NS_LITERAL_CSTRING("Push"),
                                                       nullptr, /* aDocument */
                                                       nullptr, /* aURI */
                                                       EmptyString(), /* aLine */
                                                       0, /* aLineNumber */
                                                       0, /* aColumnNumber */
                                                       nsContentUtils::eOMIT_LOCATION);
  }
  // For service worker subscriptions, report the error to all clients.
  RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
  if (swm) {
    swm->ReportToAllClients(mScope,
                            mMessage,
                            NS_ConvertUTF8toUTF16(mScope), /* aFilename */
                            EmptyString(), /* aLine */
                            0, /* aLineNumber */
                            0, /* aColumnNumber */
                            mFlags);
  }
  return NS_OK;
}

bool
PushErrorDispatcher::SendToParent(ContentChild*)
{
  return true;
}

bool
PushErrorDispatcher::SendToChild(ContentParent* aContentActor)
{
  return aContentActor->SendPushError(mScope, IPC::Principal(mPrincipal),
                                      mMessage, mFlags);
}

nsresult
PushErrorDispatcher::HandleNoChildProcesses()
{
  // Report to the console if no content processes are active.
  nsCOMPtr<nsIURI> scopeURI;
  nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), mScope);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }
  return nsContentUtils::ReportToConsoleNonLocalized(mMessage,
                                                     mFlags,
                                                     NS_LITERAL_CSTRING("Push"),
                                                     nullptr, /* aDocument */
                                                     scopeURI, /* aURI */
                                                     EmptyString(), /* aLine */
                                                     0, /* aLineNumber */
                                                     0, /* aColumnNumber */
                                                     nsContentUtils::eOMIT_LOCATION);
}

} // namespace dom
} // namespace mozilla
